/******************************************************************************* * Copyright (c) 2015 IBH SYSTEMS GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBH SYSTEMS GmbH - initial API and implementation *******************************************************************************/ package org.eclipse.packagedrone.repo.importer.aether; import static java.util.Optional.empty; import static java.util.Optional.of; import static org.eclipse.aether.util.artifact.JavaScopes.COMPILE; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import org.eclipse.aether.RepositoryException; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.collection.CollectRequest; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyFilter; import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.DependencyVisitor; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.ArtifactRequest; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.ArtifactResult; import org.eclipse.aether.resolution.DependencyRequest; import org.eclipse.aether.resolution.DependencyResult; import org.eclipse.aether.util.filter.DependencyFilterUtils; import org.eclipse.packagedrone.repo.MetaKey; import org.eclipse.packagedrone.repo.importer.ImportContext; import org.eclipse.packagedrone.repo.importer.ImportSubContext; import org.eclipse.packagedrone.repo.importer.Importer; import org.eclipse.packagedrone.repo.importer.ImporterDescription; import org.eclipse.packagedrone.repo.importer.SimpleImporterDescription; import org.eclipse.packagedrone.repo.importer.aether.web.AetherResult; import org.eclipse.packagedrone.web.LinkTarget; import org.eclipse.scada.utils.io.RecursiveDeleteVisitor; import com.google.gson.GsonBuilder; public class AetherImporter implements Importer { public static final String ID = "aether"; private static final SimpleImporterDescription DESCRIPTION = new SimpleImporterDescription (); static { DESCRIPTION.setId ( ID ); DESCRIPTION.setLabel ( "Maven 2 Importer" ); DESCRIPTION.setDescription ( "Import artifacts from Maven Repositories using Eclipse Aether" ); DESCRIPTION.setStartTarget ( new LinkTarget ( "/import/{token}/aether/start" ) ); } private final GsonBuilder gsonBuilder; public AetherImporter () { this.gsonBuilder = new GsonBuilder (); } @Override public ImporterDescription getDescription () { return DESCRIPTION; } @Override public void runImport ( final ImportContext context, final String configuration ) throws Exception { final ImportConfiguration cfg = this.gsonBuilder.create ().fromJson ( configuration, ImportConfiguration.class ); runImport ( context, cfg ); } private void runImport ( final ImportContext context, final ImportConfiguration cfg ) throws Exception { final Path tmpDir = Files.createTempDirectory ( "aether" ); context.addCleanupTask ( () -> { Files.walkFileTree ( tmpDir, new RecursiveDeleteVisitor () ); Files.deleteIfExists ( tmpDir ); } ); final Collection<ArtifactResult> results = processImport ( tmpDir, cfg ); final List<ArtifactResult> later = new LinkedList<> (); final Map<String, ImportSubContext> roots = new HashMap<> (); for ( final ArtifactResult result : results ) { if ( !result.isResolved () ) { continue; } importArtifact ( context, result, roots, later ); } // try sub artifacts again for ( final ArtifactResult result : later ) { importArtifact ( context, result, roots, null ); } } private void importArtifact ( final ImportContext context, final ArtifactResult result, final Map<String, ImportSubContext> roots, final List<ArtifactResult> later ) { final Artifact artifact = result.getArtifact (); final Map<MetaKey, String> metadata = makeMetaData ( artifact ); final String key = makeRootKey ( artifact ); if ( later != null && key != null && artifact.getClassifier () != null && !artifact.getClassifier ().isEmpty () ) { final ImportSubContext sub = roots.get ( key ); if ( sub == null ) { later.add ( result ); } else { sub.scheduleImport ( artifact.getFile ().toPath (), false, artifact.getFile ().getName (), metadata ); } } else { final ImportSubContext sub = context.scheduleImport ( artifact.getFile ().toPath (), false, artifact.getFile ().getName (), metadata ); if ( key != null ) { roots.put ( key, sub ); } } } private String makeRootKey ( final Artifact artifact ) { if ( !"jar".equals ( artifact.getExtension () ) ) { return null; } return String.format ( "%s:%s:%s", artifact.getGroupId (), artifact.getArtifactId (), artifact.getBaseVersion () ); } private static Map<MetaKey, String> makeMetaData ( final Artifact artifact ) { final Map<MetaKey, String> md = new HashMap<> (); md.put ( new MetaKey ( "mvn", "groupId" ), artifact.getGroupId () ); md.put ( new MetaKey ( "mvn", "artifactId" ), artifact.getArtifactId () ); md.put ( new MetaKey ( "mvn", "version" ), artifact.getVersion () ); md.put ( new MetaKey ( "mvn", "extension" ), artifact.getExtension () ); if ( artifact.getClassifier () != null ) { md.put ( new MetaKey ( "mvn", "classifier" ), artifact.getClassifier () ); } return md; } /** * Prepare an import with dependencies * <p> * This method does resolve even transient dependencies and also adds the * sources if requested * </p> */ public static AetherResult prepareDependencies ( final Path tmpDir, final ImportConfiguration cfg ) throws RepositoryException { Objects.requireNonNull ( tmpDir ); Objects.requireNonNull ( cfg ); final RepositoryContext ctx = new RepositoryContext ( tmpDir, cfg.getRepositoryUrl (), cfg.isAllOptional () ); // add all coordinates final CollectRequest cr = new CollectRequest (); cr.setRepositories ( ctx.getRepositories () ); for ( final MavenCoordinates coords : cfg.getCoordinates () ) { final Dependency dep = new Dependency ( new DefaultArtifact ( coords.toString () ), COMPILE ); cr.addDependency ( dep ); } final DependencyFilter filter = DependencyFilterUtils.classpathFilter ( COMPILE ); final DependencyRequest deps = new DependencyRequest ( cr, filter ); // resolve final DependencyResult dr = ctx.getSystem ().resolveDependencies ( ctx.getSession (), deps ); final List<ArtifactResult> arts = dr.getArtifactResults (); if ( !cfg.isIncludeSources () ) { // we are already done here return asResult ( arts, cfg, of ( dr ) ); } // resolve sources final List<ArtifactRequest> requests = extendRequests ( arts.stream ().map ( ArtifactResult::getRequest ), ctx, cfg ); return asResult ( resolve ( ctx, requests ), cfg, of ( dr ) ); } /** * Prepare a plain import process * <p> * Prepare a simple import request with a specific list of coordinates * </p> */ public static AetherResult preparePlain ( final Path tmpDir, final ImportConfiguration cfg ) throws ArtifactResolutionException { Objects.requireNonNull ( tmpDir ); Objects.requireNonNull ( cfg ); final RepositoryContext ctx = new RepositoryContext ( tmpDir, cfg.getRepositoryUrl (), cfg.isAllOptional () ); // extend final List<ArtifactRequest> requests = extendRequests ( cfg.getCoordinates ().stream ().map ( c -> { final DefaultArtifact artifact = new DefaultArtifact ( c.toString () ); return makeRequest ( ctx.getRepositories (), artifact ); } ), ctx, cfg ); // process return asResult ( resolve ( ctx, requests ), cfg, empty () ); } private static List<ArtifactRequest> extendRequests ( final Stream<ArtifactRequest> requests, final RepositoryContext ctx, final ImportConfiguration cfg ) { final List<ArtifactRequest> result = new LinkedList<> (); final Iterator<ArtifactRequest> i = requests.iterator (); while ( i.hasNext () ) { final ArtifactRequest ar = i.next (); result.add ( ar ); if ( cfg.isIncludeSources () ) { final DefaultArtifact sources = makeOtherClassifier ( ar.getArtifact (), "sources" ); if ( sources != null ) { result.add ( makeRequest ( ctx.getRepositories (), sources ) ); } } if ( cfg.isIncludeJavadoc () ) { final DefaultArtifact javadoc = makeOtherClassifier ( ar.getArtifact (), "javadoc" ); if ( javadoc != null ) { result.add ( makeRequest ( ctx.getRepositories (), javadoc ) ); } } if ( cfg.isIncludePoms () ) { final DefaultArtifact pom = makeOtherExtension ( ar.getArtifact (), "pom" ); if ( pom != null ) { result.add ( makeRequest ( ctx.getRepositories (), pom ) ); } } } return result; } protected static List<ArtifactResult> resolve ( final RepositoryContext ctx, final List<ArtifactRequest> requests ) { try { return ctx.getSystem ().resolveArtifacts ( ctx.getSession (), requests ); } catch ( final ArtifactResolutionException e ) { return e.getResults (); } } /** * Process the actual import request * <p> * This method takes the import configuration as is and simply tries to * import it. Not manipulating the list of coordinates any more * </p> */ public static Collection<ArtifactResult> processImport ( final Path tmpDir, final ImportConfiguration cfg ) throws ArtifactResolutionException { Objects.requireNonNull ( tmpDir ); Objects.requireNonNull ( cfg ); final RepositoryContext ctx = new RepositoryContext ( tmpDir, cfg.getRepositoryUrl () ); final Collection<ArtifactRequest> requests = new LinkedList<> (); for ( final MavenCoordinates coords : cfg.getCoordinates () ) { // main artifact final DefaultArtifact main = new DefaultArtifact ( coords.toString () ); requests.add ( makeRequest ( ctx.getRepositories (), main ) ); } // process return ctx.getSystem ().resolveArtifacts ( ctx.getSession (), requests ); } /** * Convert aether result list to AetherResult object * * @param results * the result collection * @param cfg * the import configuration * @param dependencyResult * The result of the dependency resolution * @return the AetherResult object */ public static AetherResult asResult ( final Collection<ArtifactResult> results, final ImportConfiguration cfg, final Optional<DependencyResult> dependencyResult ) { final AetherResult result = new AetherResult (); // create set of requested coordinates final Set<String> requested = new HashSet<> ( cfg.getCoordinates ().size () ); for ( final MavenCoordinates mc : cfg.getCoordinates () ) { requested.add ( mc.toString () ); } // generate dependency map final Map<String, Boolean> optionalDeps = new HashMap<> (); fillOptionalDependenciesMap ( dependencyResult, optionalDeps ); // convert artifacts for ( final ArtifactResult ar : results ) { final AetherResult.Entry entry = new AetherResult.Entry (); final MavenCoordinates coordinates = MavenCoordinates.fromResult ( ar ); final String key = coordinates.toBase ().toString (); entry.setCoordinates ( coordinates ); entry.setResolved ( ar.isResolved () ); entry.setRequested ( requested.contains ( key ) ); entry.setOptional ( optionalDeps.getOrDefault ( key, Boolean.FALSE ) ); // convert error if ( ar.getExceptions () != null && !ar.getExceptions ().isEmpty () ) { final StringBuilder sb = new StringBuilder ( ar.getExceptions ().get ( 0 ).getMessage () ); if ( ar.getExceptions ().size () > 1 ) { sb.append ( " ..." ); } entry.setError ( sb.toString () ); } // add to list result.getArtifacts ().add ( entry ); } // sort by coordinates Collections.sort ( result.getArtifacts (), Comparator.comparing ( AetherResult.Entry::getCoordinates ) ); // set repo url result.setRepositoryUrl ( cfg.getRepositoryUrl () ); return result; } private static void fillOptionalDependenciesMap ( final Optional<DependencyResult> dependencyResult, final Map<String, Boolean> optionalDeps ) { if ( !dependencyResult.isPresent () ) { return; } dependencyResult.get ().getRoot ().accept ( new DependencyVisitor () { @Override public boolean visitLeave ( final DependencyNode node ) { return true; } @Override public boolean visitEnter ( final DependencyNode node ) { final Dependency d = node.getDependency (); if ( d == null ) { return true; } final String key = MavenCoordinates.fromArtifact ( d.getArtifact () ).toBase ().toString (); if ( d.isOptional () ) { if ( !optionalDeps.containsKey ( key ) ) { optionalDeps.put ( key, Boolean.TRUE ); } } else { optionalDeps.put ( key, Boolean.FALSE ); } return true; } } ); } private static DefaultArtifact makeOtherClassifier ( final Artifact main, final String classifier ) { if ( main.getClassifier () != null && !main.getClassifier ().isEmpty () ) { // we only change main artifacts return null; } return new DefaultArtifact ( main.getGroupId (), main.getArtifactId (), classifier, main.getExtension (), main.getVersion () ); } private static DefaultArtifact makeOtherExtension ( final Artifact main, final String extension ) { if ( main.getClassifier () != null && !main.getClassifier ().isEmpty () ) { // we only change main artifacts return null; } return new DefaultArtifact ( main.getGroupId (), main.getArtifactId (), null, extension, main.getVersion () ); } private static ArtifactRequest makeRequest ( final List<RemoteRepository> repositories, final Artifact artifact ) { final ArtifactRequest artifactRequest = new ArtifactRequest (); artifactRequest.setArtifact ( artifact ); artifactRequest.setRepositories ( repositories ); return artifactRequest; } }